﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace SimpleData.ObjectModel.Collections
{
	/// <summary>
	/// Lista de tamanho fixo que armazena os objetos mais utilizados e descarta os que forma
	/// menos acessados
	/// </summary>
	/// <typeparam name="TKey"></typeparam>
	/// <typeparam name="TValue"></typeparam>
	public sealed class MruList<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>, IDisposable
	{
		private Dictionary<TKey, MruListBucket> indexes;
		private ReaderWriterLockSlim valuesLock;
		private MruListBucket mostRecent;
		private MruListBucket leastRecent;
		private int limit;

		#region [ .ctor ]

		/// <summary>
		/// Inicializa uma nova lista
		/// </summary>
		/// <param name="limit">Quantidade máxima de itens da fila</param>
		public MruList( int limit )
		{
			this.indexes = new Dictionary<TKey, MruListBucket>( limit );
			this.valuesLock = new ReaderWriterLockSlim();
			this.limit = limit;
		}

		#endregion

		/// <summary>
		/// Obtém o total de elementos na lista
		/// </summary>
		public int Count
		{
			get { return this.indexes.Count; }
		}

		/// <summary>
		/// Obtém a quantidade máxima que a lista pode ter
		/// </summary>
		public int Limit
		{
			get { return this.limit; }
		}

		/// <summary>
		/// Indica se a lista atingiu o limite
		/// </summary>
		public bool ReachLimit
		{
			get { return this.Limit == this.Count; }
		}

		/// <summary>
		/// Obtém o primeiro elemento da lista. O elemento que foi
		/// acessado recentemente
		/// </summary>
		public KeyValuePair<TKey, TValue> MostRecent
		{
			get
			{
				if ( this.mostRecent == null )
					throw new InvalidOperationException( "A lista está vazia." );
				else
				{
					var f = this.mostRecent;
					return new KeyValuePair<TKey, TValue>( f.Key, f.Value );
				}
			}
		}

		/// <summary>
		/// Obtém o último elemento da lista. O elemento que foi 
		/// menos acessado
		/// </summary>
		public KeyValuePair<TKey, TValue> LessRecent
		{
			get
			{
				if ( this.leastRecent == null )
					throw new InvalidOperationException( "A lista está vazia." );
				else
				{
					var bucket = this.leastRecent;
					return new KeyValuePair<TKey, TValue>( bucket.Key, bucket.Value );
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public TValue this[ TKey key ]
		{
			get { return this.Get( key ); }
			set { this.Set( key, value ); }
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		public void Add( TKey key, TValue value )
		{
			this.Set( key, value );
		}

		/// <summary>
		/// Adiciona o <paramref name="value"/> na lista
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		public void Set( TKey key, TValue value )
		{
			using ( this.valuesLock.EnterWritingMode() )
			{
				if ( this.ReachLimit )
					this.RemoveLessRecentlyBucket();

				this.CreateBucketOnFirst( key, value );
			}
		}

		/// <summary>
		/// Cria um novo slot para armazenar o valor <paramref name="value"/>
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		private void CreateBucketOnFirst( TKey key, TValue value )
		{
			var bucket = new MruListBucket();

			bucket.Key = key;
			bucket.Value = value;

			// Move para a primeira posição
			this.MoveToFirst( bucket );

			// Se está vazia, também vai ser o último elemento
			if ( this.leastRecent == null )
				this.leastRecent = bucket;

			// Atualiza o índice
			this.indexes[ key ] = bucket;
		}

		/// <summary>
		/// Retira o último slot (o menos acessado)
		/// </summary>
		private void RemoveLessRecentlyBucket()
		{
			var deleted = this.leastRecent;

			this.leastRecent = deleted.Previous;

			// Se o last é null, então removeu todos os buckets
			if ( this.leastRecent != null )
				this.leastRecent.Next = null;
			else
				this.mostRecent = null;

			this.indexes.Remove( deleted.Key );
		}

		/// <summary>
		/// Move esse <paramref name="bucket"/> para a primeira posição
		/// da fila.
		/// </summary>
		/// <param name="bucket"></param>
		private void MoveToFirst( MruListBucket bucket )
		{
			// o bucket já está em primeiro da lista
			if ( bucket.Equals( this.mostRecent ) )
				return;

			// O próximo da fila deve apontar para o anterior
			// do bucket que está sendo movido
			if ( bucket.Next != null )
				bucket.Next.Previous = bucket.Previous;
			// O anterior da lista, deve apontar
			// para o proximo do bucket
			if ( bucket.Previous != null )
				bucket.Previous.Next = bucket.Next;

			bucket.Next = this.mostRecent;

			// Move o primeiro para o segundo da lista
			if ( this.mostRecent != null )
				this.mostRecent.Previous = bucket;

			// Se o bucket é o último, 
			// então o anterior a ele vai para a última posição
			if ( bucket.Equals( this.leastRecent ) )
				this.leastRecent = bucket.Previous;

			this.mostRecent = bucket;
			bucket.Previous = null;
			bucket.LastAccess = DateTime.Now;
		}

		/// <summary>
		/// Tenta obter o valor associado à chave <paramref name="key"/> e
		/// marca essa chave como a que foi acessada por último
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public bool TryGetValue( TKey key, out TValue value )
		{
			MruListBucket bucket;

			using ( this.valuesLock.EnterUpgradeableMode() )
			{
				// Se conseguiu pegar a chave, marca o item 
				// como o mais acessado recentemente
				if ( this.indexes.TryGetValue( key, out bucket ) )
				{
					value = bucket.Value;
					if ( !this.mostRecent.Equals( bucket ) )
						using ( this.valuesLock.EnterWritingMode() )
							this.MoveToFirst( bucket );
					return true;
				}
			}
			value = default( TValue );
			return false;
		}

		/// <summary>
		/// Obtém o valor associado à chave <paramref name="key"/>
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public TValue Get( TKey key )
		{
			TValue value;

			if ( this.TryGetValue( key, out value ) )
				return value;
			else
				throw new InvalidOperationException( String.Format( "A chave \"{0}\" não existe na lista.", Convert.ToString( key ) ) );
		}

		/// <summary>
		/// Obtém o valor da lista ou adiciona um novo caso não exista
		/// </summary>
		/// <param name="key"></param>
		/// <param name="ifNotExists"></param>
		/// <returns></returns>
		public TValue GetOrAdd( TKey key, Func<TValue> ifNotExists )
		{
			TValue value;

			// Tenta obter
			if ( !this.TryGetValue( key, out value ) )
			{
				// Se não existe adiciona
				value = ifNotExists();
				this.Add( key, value );
			}
			return value;
		}

		/// <summary>
		/// Libera os recursos utilizados pela lista
		/// </summary>
		public void Dispose()
		{
			if ( this.valuesLock != null )
				this.valuesLock.Dispose();
		}

		/// <summary>
		/// Lista todos os elemento na ordem em que foram acessados recentemente
		/// </summary>
		/// <returns></returns>
		public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
		{
			var bucket = this.mostRecent;

			while ( bucket != null )
			{
				yield return new KeyValuePair<TKey, TValue>( bucket.Key, bucket.Value );
				bucket = bucket.Next;
			}
		}

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return this.GetEnumerator();
		}

		#region [ class MruListBucket ]

		/// <summary>
		/// Representa um slot (espaço) na lista encadeada
		/// </summary>
		private class MruListBucket
		{
			/// <summary>
			/// Armazena a data do último acesso
			/// </summary>
			public DateTime LastAccess
			{ get; set; }

			/// <summary>
			/// Obtém o próximo item da lista
			/// </summary>
			public MruListBucket Next
			{ get; set; }

			/// <summary>
			/// Obtém ou define o item anterior da lista
			/// </summary>
			public MruListBucket Previous
			{ get; set; }

			public TKey Key
			{ get; set; }

			public TValue Value
			{ get; set; }
		}

		#endregion
	}
}